﻿//This code is a direct copy from http://somewebguy.wordpress.com/2009/08/12/using-webcontrols-in-mvc-part-3/
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Compilation;
using System.Web.Mvc;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Xml.Linq;
using System.Xml.XPath;

namespace EPiMVC.WebForms {

    #region Extension Methods

    /// <summary>
    /// Class to allow WebForm Controls to be used inline with Mvc
    /// </summary>
    public static class MvcWebFormExtensionMethods {

        #region Constants

        internal const string RENDER_CONTROL_PREFIX = "MVCWEBCONTROL";
        internal const string VIEWSTATE_ALTERNATE_NAME = "MVCSTATE";
        internal const string DEFAULT_FORM_ID = "pf";

        #endregion

        #region Creating The WebForm

        /// <summary>
        /// Starts a new WebForm inline for your Mvc page
        /// </summary>
        public static void WebForm(this HtmlHelper html, Action<MvcWebForm> build) {
            MvcWebFormExtensionMethods.WebForm(html, MvcWebFormExtensionMethods.DEFAULT_FORM_ID, new { }, build);
        }

        /// <summary>
        /// Starts a new WebForm inline for your Mvc page
        /// </summary>
        public static void WebForm(this HtmlHelper html, object props, Action<MvcWebForm> build) {
            MvcWebFormExtensionMethods.WebForm(html, MvcWebFormExtensionMethods.DEFAULT_FORM_ID, props, build);
        }

        /// <summary>
        /// Starts a new WebForm inline for your Mvc page
        /// </summary>
        public static void WebForm(this HtmlHelper html, string id, Action<MvcWebForm> build) {
            MvcWebFormExtensionMethods.WebForm(html, id, new { }, build);
        }

        /// <summary>
        /// Starts a new WebForm inline for your Mvc page
        /// </summary>
        public static void WebForm(this HtmlHelper html, string id, object props, Action<MvcWebForm> build) {

            //build the rendering control
            _WebFormRenderControl render = new _WebFormRenderControl(id, html, props, build);
            render.ProcessForm();

        }

        #endregion

        #region Rendering Handler

        /// <summary>
        /// Class to handle rendering a WebForm onto a Mvc View
        /// </summary>
        internal sealed class _WebFormRenderControl {

            #region Constants

            internal const string FORM_METHOD_ATTRIBUTE = "method";
            internal const string FORM_ENCTYPE_ATTRIBUTE = "enctype";
            internal const string FORM_ACTION_ATTRIBUTE = "action";
            internal const string FORM_MAP_VALUES_MARKER = "MVCMAPVALUES";

            private const string FORM_TOP_SCRIPTS = "MVCTOPSCRIPTS";
            private const string FORM_BOTTOM_SCRIPTS = "MVCBOTTOMSCRIPTS";
            private const string FORM_ATTRIBUTES_MARKER = "MVCFORMATTRIBUTES";
            private const string TEMPLATE_PAGE = "~/Views/MvcWebFormPage.aspx";
            private const string CONTROL_RENDER_TAG = "render";
            private const string CONTROL_HIDE_TAG = "dontrender";
            private const string ATTRIBUTE_ID = "id";
            private const string ATTRIBUTE_NAME = "name";
            private const string ATTRIBUTE_VALUE = "value";
            private const string VIEWSTATE_NAME = "__VIEWSTATE";
            private const string XPATH_TO_FORM = "html/body/form";
            private const string XPATH_TO_HEAD = "html/head";
            private const string TOO_MANY_WEBFORMS = "You can only have one MvcWebForm per page (just like with regular WebForms)";
            private const string FORM_OPEN_TAG = "<form ";
            private const string FORM_OPEN_TAG_END = ">";
            private const string FORM_CLOSE_TAG = "</form>";
            private const string FORM_ATTRIBUTE_FORMAT = "{0}=\"{1}\" ";
            private const string DEFAULT_FORM_ENCTYPE = "multipart/form-data";
            private const string DEFAULT_FORM_METHOD = "post";
            private readonly static string[] IGNORED_TAGS = { "title", "head", "body" };

            #endregion

            #region Constructors

            /// <summary>
            /// Creates a new WebFormRenderControl to process a WebForm in the background
            /// </summary>
            public _WebFormRenderControl(string id, HtmlHelper html, object properties, Action<MvcWebForm> build) {
                this._ID = id;
                this._Html = html;
                this._Properties = properties;
                this._Build = build;
            }

            #endregion

            #region Private Fields

            private string _ID;
            private MvcWebFormPage _Page;
            private MvcWebForm _Container;
            private HtmlHelper _Html;
            private object _Properties;

            //holds the action for rendering the page
            private Action<MvcWebForm> _Build;

            #endregion

            #region Methods

            /// <summary>
            /// Processes a WebForm and controls
            /// </summary>
            public void ProcessForm() {

                //make sure there isn't a WebForm already created
                if (HttpContext.Current.Response.Filter is _WebFormStreamFilter) {
                    throw new InvalidOperationException(TOO_MANY_WEBFORMS);
                }

                //load the container and add the control
                this._Page = (MvcWebFormPage)
                    BuildManager.CreateInstanceFromVirtualPath(
                    TEMPLATE_PAGE,
                    typeof(MvcWebFormPage)
                    );

                //begin building the form
                HttpContext.Current.Response.Write(
                    string.Format(
                        FORM_OPEN_TAG,
                        this._Html.Encode(this._ID)
                        ));

                //add a content place holder for attributes
                MvcWebFormExtensionMethods._WebFormStreamFilter.Filter.Insert(FORM_ATTRIBUTES_MARKER);

                //close up the rest of the tag
                HttpContext.Current.Response.Write(FORM_OPEN_TAG_END);

                //add a marker for the top scripts
                MvcWebFormExtensionMethods._WebFormStreamFilter.Filter.Insert(FORM_TOP_SCRIPTS);

                //use the a WebFormContainer to help render content
                this._Container = new MvcWebForm(this._Page);
                MvcWebFormExtensionMethods._WebFormStreamFilter.Filter.Form = this._Container;

                //update the attributes for the builder
                foreach (PropertyInfo prop in this._Properties.GetType().GetProperties()) {
                    if (!prop.CanRead) { continue; }
                    this._Container.Attributes[prop.Name] = prop.GetValue(this._Properties, null);
                }

                //perform the build logic
                this._Build(this._Container);

                //finish up the form
                MvcWebFormExtensionMethods._WebFormStreamFilter.Filter.Insert(FORM_BOTTOM_SCRIPTS);

                //and a spot for any mapped values
                MvcWebFormExtensionMethods._WebFormStreamFilter.Filter.Insert(FORM_MAP_VALUES_MARKER);
                HttpContext.Current.Response.Write(FORM_CLOSE_TAG);


                //before rendering, make sure there is a Method attribute -- if not, default to POST
                if (!this._Container.Attributes.Keys.Any(o =>
                    o.Equals(FORM_METHOD_ATTRIBUTE, StringComparison.OrdinalIgnoreCase))) {
                    this._Container.Attributes[FORM_METHOD_ATTRIBUTE] = DEFAULT_FORM_METHOD;
                }

                //also check for an enctype
                if (!this._Container.Attributes.Keys.Any(o =>
                    o.Equals(FORM_ENCTYPE_ATTRIBUTE, StringComparison.OrdinalIgnoreCase))) {
                    this._Container.Attributes[FORM_ENCTYPE_ATTRIBUTE] = DEFAULT_FORM_ENCTYPE;
                }

                //assign the handlers
                this._Page.Init += _HandleInitPage;
                this._Page.Rendered += _HandleRenderPage;

                //process the container page
                ((IHttpHandler)this._Page).ProcessRequest(
                    new HttpContext(
                        HttpContext.Current.Request,
                        new HttpResponse(new StringWriter())
                        ));

            }

            //initializes the page for use, such as adding controls
            private void _HandleInitPage(object sender, EventArgs e) {

                //update the form ID
                this._Page.Form.ID = this._ID;

                //append each control to the template render
                foreach (Control control in this._Container._Controls) {
                    HtmlGenericControl holder = new HtmlGenericControl(CONTROL_RENDER_TAG) {
                        ID = string.Concat(
                            RENDER_CONTROL_PREFIX,
                            this._Container._Controls.IndexOf(control).ToString()
                            )
                    };
                    holder.Controls.Add(control);
                    this._Page.Form.Controls.Add(holder);
                }

                //also, append a hidden Calendar control to ensure that
                //scripts are loaded as needed
                HtmlGenericControl hidden = new HtmlGenericControl(CONTROL_HIDE_TAG);
                hidden.Controls.Add(
                    new Calendar() {
                        EnableViewState = false
                    });
                this._Page.Form.Controls.Add(hidden);

            }

            //renders the output to the view to work for Mvc and WebForms
            private void _HandleRenderPage(string output) {

                //load the document so it can be worked with
                XDocument doc = XDocument.Parse(output.Trim());

                //update the viewstate information
                foreach (var item in doc.Descendants()
                    .Where(o => o.Attribute(ATTRIBUTE_NAME) is XAttribute &&
                        o.Attribute(ATTRIBUTE_NAME).Value
                            .Equals(VIEWSTATE_NAME, StringComparison.OrdinalIgnoreCase))) {

                    //update the viewstate information
                    item.Attribute(ATTRIBUTE_NAME).Value = VIEWSTATE_ALTERNATE_NAME;
                    item.Attribute(ATTRIBUTE_VALUE).Value = this._Page.MvcState;

                    //fix the id (not sure where the space is coming from)
                    item.Attribute(ATTRIBUTE_ID).Value = item.Attribute(ATTRIBUTE_ID).Value.Trim();
                }

                //get all of the rendered content
                XElement[] elements = doc.Descendants()
                    .Where(o => o.Name.LocalName.Equals(CONTROL_RENDER_TAG))
                    .ToArray();

                //loop backwards and add the output to the page
                for (int i = elements.Count(); i-- > 0; ) {

                    //append each element to the correct marker
                    string controlId = elements[i].Attribute(ATTRIBUTE_ID).Value;
                    foreach (XElement child in elements[i].Elements()) {
                        MvcWebFormExtensionMethods._WebFormStreamFilter.Filter.AppendTo(
                            controlId,
                            child.ToString()
                            );
                    }

                    //and remove the item to prevent duplicates
                    elements[i].Remove();
                }

                //append any form attributes that may have been added by the page
                foreach (XAttribute attribute in doc.XPathSelectElement(XPATH_TO_FORM).Attributes()) {
                    if (attribute.Name.LocalName.Equals(FORM_ACTION_ATTRIBUTE, StringComparison.OrdinalIgnoreCase)) { continue; }
                    this._Container.Attributes[attribute.Name.LocalName] = attribute.Value;
                }

                //update the attributes
                StringBuilder attributes = new StringBuilder();
                foreach (KeyValuePair<string, object> pair in this._Container.Attributes) {
                    MvcWebFormExtensionMethods._WebFormStreamFilter.Filter.AppendTo(
                        FORM_ATTRIBUTES_MARKER,
                            string.Format(
                            FORM_ATTRIBUTE_FORMAT,
                            pair.Key,
                            this._Html.Encode(pair.Value)
                        ));
                }

                //next, append any header items
                foreach (XElement show in doc.XPathSelectElement(XPATH_TO_HEAD).Elements()) {

                    //prevent any unwanted types
                    if (IGNORED_TAGS.Any(o => o.Equals(show.Name.LocalName, StringComparison.OrdinalIgnoreCase) ||
                        show.Name.LocalName.Equals(CONTROL_HIDE_TAG))) { continue; }
                    MvcWebFormExtensionMethods._WebFormStreamFilter.Filter.AppendTo(
                        FORM_BOTTOM_SCRIPTS,
                        show.ToString()
                        );
                }

                //check for our 'donotrender' tag, which identifies the halfway
                //point on our page for the rendered controls and the scripts
                bool renderElementAtTop = true;
                foreach (XElement show in doc.XPathSelectElement(XPATH_TO_FORM).Elements()) {
                    if (show.Name.LocalName.Equals(CONTROL_HIDE_TAG)) { renderElementAtTop = false; }

                    //prevent any unwanted types
                    if (IGNORED_TAGS.Any(o => o.Equals(show.Name.LocalName, StringComparison.OrdinalIgnoreCase)) ||
                        show.Name.LocalName.Equals(CONTROL_HIDE_TAG, StringComparison.OrdinalIgnoreCase)) { continue; }

                    //append the script information
                    MvcWebFormExtensionMethods._WebFormStreamFilter.Filter.AppendTo(
                        renderElementAtTop ? FORM_TOP_SCRIPTS : FORM_BOTTOM_SCRIPTS,
                        show.ToString()
                        );
                }

            }

            #endregion

        }

        #endregion

        #region Output Fitler Stream

        //handles catching and formatting data before it is
        //send out as rendered HTML
        internal sealed class _WebFormStreamFilter : MemoryStream {

            #region Constants

            private const string MUST_USE_WEBFORM_CALL_FIRST = "You must call HtmlHelper.WebForm() before you can add controls to the page.";
            private const string REGEX_MATCH_MARKERS = @"\{{3}\:{2}(?<name>\w+)\:{2}\}{3}";
            private const string REGEX_MATCH_GROUP_NAME = "name";
            private const string MARKER_START = "{{{::";
            private const string MARKER_END = "::}}}";

            #endregion

            #region Constructors

            public _WebFormStreamFilter() {
                this._Output = new StringBuilder();
                this._Markers = new Dictionary<string, string>();
            }

            #endregion

            #region Properties

            //verifies the filter has been setup to catch the markers
            public static _WebFormStreamFilter Filter {
                get {
                    if (!(HttpContext.Current.Response.Filter is _WebFormStreamFilter)) {
                        HttpContext.Current.Response.Filter = new _WebFormStreamFilter();
                    }
                    return (_WebFormStreamFilter)HttpContext.Current.Response.Filter;
                }
            }

            #endregion

            #region Members

            /// <summary>
            /// Returns the instance of the MvcWebForm being used to generate this content
            /// </summary>
            public MvcWebForm Form {
                get {
                    if (this._Form == null) {
                        throw new InvalidOperationException(MUST_USE_WEBFORM_CALL_FIRST);
                    }
                    return this._Form;
                }
                internal set {
                    this._Form = value;
                }
            }
            private MvcWebForm _Form;

            StringBuilder _Output;
            Dictionary<string, string> _Markers;

            #endregion

            #region Methods

            //override the write event to capture the string
            public override void Write(byte[] buffer, int offset, int count) {
                string content = HttpContext.Current.Response.ContentEncoding.GetString(buffer, offset, count);
                _Output.Append(content);
            }

            //inserts a marker into the content to replace later
            public void Insert(string identity) {
                string marker = string.Concat(
                    _WebFormStreamFilter.MARKER_START,
                    identity,
                    _WebFormStreamFilter.MARKER_END
                    );
                HttpContext.Current.Response.Write(marker);
            }

            //append content that will be used to replace a marker
            public void AppendTo(string identity, string content) {
                if (!_Markers.ContainsKey(identity)) {
                    _Markers.Add(identity, string.Empty);
                }
                _Markers[identity] = string.Concat(_Markers[identity], content);
            }

            #endregion

            #region Overrides

            //overrides the dispose event to render our final content
            protected override void Dispose(bool disposing) {

                //if this is disposing, replace the content now
                if (disposing) {
                    string render = _Output.ToString();

                    //loop backwards and replace the markers
                    MatchCollection matches = Regex.Matches(render, _WebFormStreamFilter.REGEX_MATCH_MARKERS);
                    for (int i = matches.Count; i-- > 0; ) {
                        Match match = matches[i];

                        //find the replacement
                        string key = match.Groups[_WebFormStreamFilter.REGEX_MATCH_GROUP_NAME].Value;
                        string replacement = string.Empty;
                        if (this._Markers.ContainsKey(key)) {
                            replacement = this._Markers[key];
                        }

                        //remove the match and insert the content
                        render = render.Remove(match.Index, match.Length);
                        render = render.Insert(match.Index, replacement);
                    }

                    //write the final content to the output
                    HttpContext.Current.Response.Write(render);
                }
            }

            #endregion

        }

        #endregion

    }

    #endregion

    #region Rendering And Building

    /// <summary>
    /// Page rendering class for WebControl output
    /// </summary>
    public class MvcWebFormPage : Page {

        //notify the calling parent the HTML is ready
        public event Action<string> Rendered;
        public string MvcState {
            get {
                return this._MvcState;
            }
        }
        string _MvcState;


        //manually loads the viewstate
        protected override object LoadPageStateFromPersistenceMedium() {

            //check for the content in the Request
            string content = HttpContext.Current.Request[MvcWebFormExtensionMethods.VIEWSTATE_ALTERNATE_NAME] as string;

            //if nothing was loaded, just give up
            if (content == null) { return new object(); }

            //return the formatted object
            LosFormatter format = new LosFormatter();
            return format.Deserialize(content);
        }

        //writes the viewstate to a separate string
        protected override void SavePageStateToPersistenceMedium(object state) {
            StringWriter writer = new StringWriter();
            LosFormatter format = new LosFormatter();
            format.Serialize(writer, state);
            this._MvcState = writer.ToString();
        }

        //render the output to work with
        protected override void Render(HtmlTextWriter writer) {
            using (StringWriter output = new StringWriter()) {
                using (HtmlTextWriter html = new HtmlTextWriter(output)) {
                    base.Render(html);
                    this.Rendered(output.ToString());
                }
            }
        }
    }

    /// <summary>
    /// Container to render controls as a group to the WebForm output
    /// </summary>
    public class MvcControlGroup : PlaceHolder {

        /// <summary>
        /// Holds the controls that should be rendered to the page
        /// </summary>
        public override ControlCollection Controls {
            get {
                return this._Content.Controls;
            }
        }
        private PlaceHolder _Content = new PlaceHolder();

        //tells this control it should render to the WebForm
        protected override void Render(HtmlTextWriter writer) {
            if (this._Content == null) { return; }
            MvcWebFormExtensionMethods._WebFormStreamFilter.Filter.Form.RenderControl(this._Content);
        }

    }

    /// <summary>
    /// Allows WebControls to be rendered inline with Mvc
    /// </summary>
    public class MvcWebForm {

        #region Constants

        private const string HIDDEN_FIELD = "<input type=\"hidden\" name=\"{0}\" value=\"{1}\" />";
        private const string EXCEPTION_COULD_NOT_MAP_VALUES = "Exception while using MvcWebForm.Map() - Unable to map values correctly.";
        private const string EXCEPTION_MUST_USE_WEBFORM_FIRST = "There is no instance of a MvcWebForm yet. Did you call HtmlHelper.WebForm() yet?";

        #endregion

        #region Constructors

        //no public constructors
        internal MvcWebForm(Page page) {
            this.Page = page;
            this.Context = HttpContext.Current;
            this.Attributes = new Dictionary<string, object>();
        }

        #endregion

        #region Accessing The Form

        /// <summary>
        /// Returns the instance of the current MvcWebForm (if any)
        /// </summary>
        public static MvcWebForm Current {
            get {
                if (MvcWebFormExtensionMethods._WebFormStreamFilter.Filter == null ||
                    MvcWebFormExtensionMethods._WebFormStreamFilter.Filter.Form == null) {
                    throw new InvalidOperationException(EXCEPTION_MUST_USE_WEBFORM_FIRST);
                }
                return MvcWebFormExtensionMethods._WebFormStreamFilter.Filter.Form;
            }
        }

        #endregion

        #region Properties and Fields

        //contains a list of controls to render
        internal List<Control> _Controls = new List<Control>();

        /// <summary>
        /// Instance of the page that is being generated
        /// </summary>
        public Page Page { get; private set; }

        /// <summary>
        /// The context for the builder
        /// </summary>
        public HttpContext Context { get; private set; }

        /// <summary>
        /// A collection of attributes to attach to the form
        /// </summary>
        public Dictionary<string, object> Attributes { get; private set; }

        #endregion

        #region Specific Actions

        /// <summary>
        /// Sets the postback method for this form
        /// </summary>
        public string Method {
            get { return this.Attributes[MvcWebFormExtensionMethods._WebFormRenderControl.FORM_METHOD_ATTRIBUTE] as string ?? string.Empty; }
            set { this.Attributes[MvcWebFormExtensionMethods._WebFormRenderControl.FORM_METHOD_ATTRIBUTE] = value; }
        }

        /// <summary>
        /// Sets the action for the Form
        /// </summary>
        public string Action {
            get { return this.Attributes[MvcWebFormExtensionMethods._WebFormRenderControl.FORM_ACTION_ATTRIBUTE] as string ?? string.Empty; }
            set { this.Attributes[MvcWebFormExtensionMethods._WebFormRenderControl.FORM_ACTION_ATTRIBUTE] = value; }
        }

        /// <summary>
        /// Sets the content type for this form
        /// </summary>
        public string ContentType {
            get { return this.Attributes[MvcWebFormExtensionMethods._WebFormRenderControl.FORM_ENCTYPE_ATTRIBUTE] as string ?? string.Empty; }
            set { this.Attributes[MvcWebFormExtensionMethods._WebFormRenderControl.FORM_ENCTYPE_ATTRIBUTE] = value; }
        }

        #endregion

        #region Rendering Methods

        /// <summary>
        /// Renders a control by name
        /// </summary>
        public Control RenderControl(string path) {
            UserControl control = (UserControl)BuildManager.CreateInstanceFromVirtualPath(
                path,
                typeof(UserControl)
                );
            this.RenderControl(control);
            return control;
        }

        /// <summary>
        /// Renders a control by name and allows changes to made to it
        /// </summary>
        public Control RenderControl(string path, Action<UserControl> setup) {
            UserControl control = (UserControl)BuildManager.CreateInstanceFromVirtualPath(
                path,
                typeof(UserControl)
                );

            //perform any setup work
            setup(control);
            this.RenderControl(control);
            return control;
        }

        /// <summary>
        /// Renders a WebControl inline for the page
        /// </summary>
        public T RenderControl<T>(T control) where T : Control {

            //place a marker to remember where this content goes
            MvcWebFormExtensionMethods._WebFormStreamFilter.Filter.Insert(
                string.Concat(
                    MvcWebFormExtensionMethods.RENDER_CONTROL_PREFIX,
                    this._Controls.Count()
                    ));

            //and add it to the list to render
            this._Controls.Add(control);
            return control;
        }

        /// <summary>
        /// Uses a delegate to generate and then return a control
        /// </summary>
        public T RenderControl<T>(Func<T> build) where T : Control {

            //use their method to generate the control and add it to the list
            T control = build();
            this.RenderControl(control);

            //return an instance of the control created
            return control;
        }

        #endregion

        #region Binding Methods

        /// <summary>
        /// Outputs a hidden field for a hidden field
        /// </summary>
        public static void Map(Func<object> assign) {

            //load the values for the user
            object bind;
            try {
                bind = assign();
            }
            catch (Exception ex) {
                throw new ArgumentException(EXCEPTION_COULD_NOT_MAP_VALUES, ex);
            }

            //assign each property
            foreach (PropertyInfo prop in bind.GetType().GetProperties()) {
                if (!prop.CanRead) { continue; }

                //try and get the value
                string output = string.Empty;
                object value = prop.GetValue(bind, null);

                //if it isn't null, convert it to a string
                if (value != null) { output = value.ToString(); }

                //write it as a hidden field                
                MvcWebFormExtensionMethods._WebFormStreamFilter.Filter.AppendTo(
                    MvcWebFormExtensionMethods._WebFormRenderControl.FORM_MAP_VALUES_MARKER,
                    string.Format(
                        HIDDEN_FIELD + Environment.NewLine,
                        HttpContext.Current.Server.HtmlEncode(prop.Name),
                        HttpContext.Current.Server.HtmlEncode(output)
                        ));
            }
        }

        #endregion

    }

    #endregion

}